See How the demo was built further down in this article.
Introduction
I'd like to share my first attempt at adding tool windows like the ones you can find in applications like MS Visio. So here is the implementation of a snappable tool window (snaps to the sides of a view window) that also provides auto-hide and pinning features, see the picture above.
Thanks to Bjarke Viksoe for his docking framework. Although this code is written from scratch, his framework got me started. See atldock.h.
Reference
CSnappingWindow
is the main window used for managing the snappable views. It is responsible for updating positions when the frame is resized etc. and provides the API for adding and positioning snappable views.
CSnappingWindow
members:
CSnappingWindow
member details:
void SetClient(HWND hWndClient) |
|
HWND hWndClient is the client view whose edge the views are snapping to |
|
"#SNAPCONTEXT">SNAPCONTEXT* AddSnappableWindow(HWND hWndView) |
|
HWND hWndView is the view to be snappable (any window) |
|
"#SNAPCONTEXT">SNAPCONTEXT* is a pointer to the snapping context structure, will be NULL if not successful |
|
Remark: The view is initially in hidden state. |
|
void FloatWindow(HWND hWndView, const POINT& ptPos, DWORD dwFlags) |
|
HWND hWndView is a view that has been added to the framework with AddSnappableWindow |
|
const POINT& ptPos top left coordinates where the window will be placed |
|
DWORD dwFlags additional flags, defaults to zero |
|
Remark: The view needs to be in a hidden state before this method is called. |
|
void SnapWindow(HWND hWndView, "#SnapPosition">SnapPosition spPos, int cxy, DWORD dwFlags) |
|
HWND hWndView is a view that has been added to the framework with AddSnappableWindow |
|
"#SnapPosition">SnapPosition spPos snapped position |
|
int cxy offset from the top or left edge depending on spPos , defaults to zero |
|
DWORD dwFlags additional flags, defaults to snapMinibar |
|
Remark: The view needs to be in a hidden state before this method is called. |
|
void HideWindow(HWND hWndView) |
|
HWND hWndView is a view that has been added to the framework with AddSnappableWindow |
SnapPosition
enumeration enforces the allowed combination of position flags.
snapFloat |
0x00000000 |
snapLeft |
0x00000001 |
snapTop |
0x00000002 |
snapRight |
0x00000004 |
snapBottom |
0x00000008 |
snapTopLeft |
0x00000003 |
snapTopRight |
0x00000006 |
snapBottomLeft |
0x00000009 |
snapBottomRight |
0x0000000C |
snapHidden |
0x00000010 |
Additional flags and masks to manage the dwFlags
attribute:
State flags |
snapPinned |
0x00000100 |
snapMinibar |
0x00000200 |
|
Masks |
snapPosition |
0x000000FF |
snapState |
0x00FFFF00 |
snapReserved |
0xFF000000 |
The flags snapPinned
and snapMinibar
have no effect when floating.
The structure SNAPCONTEXT
is the context of the snappable window.
HWND hWndSnapped |
Snapped window handle |
HWND hWndFloated |
Floating window handle |
HWND hWndView |
View window handle |
HWND hWndView |
Snap window manager |
DWORD dwFlags |
Position and state flags |
Knowledge about this structure is not important when using this framework, but for extending it.
Implementation details
The following custom Windows messages are defined for the framework:
#ifndef SNAP_MSGBASE
#define SNAP_MSGBASE WM_USER+860
#endif
#define WM_SNAP_FLOAT SNAP_MSGBASE
#define WM_SNAP_SNAP SNAP_MSGBASE + 1
#define WM_SNAP_HIDE SNAP_MSGBASE + 2
#define WM_SNAP_QUERYRECT SNAP_MSGBASE + 3
#define WM_SNAP_MOVEDONE SNAP_MSGBASE + 4
#define WM_SNAP_REPOSITION SNAP_MSGBASE + 5
#define WM_SNAP_UPDATELAYOUT SNAP_MSGBASE + 6
#define WM_SNAP_QUERYSIZE SNAP_MSGBASE + 7
One of the harder issues to solve was the dragging of windows between snapping positions and between snapped and floating state. After many failing attempts, I now manage the dragging of the windows myself. In doing so, I store the starting point of the cursor and the offset to a reference point on the window. The trick was to move the reference point and update the offset depending on which side the window is snapped to.
E.g., when snapped to the bottom right corner, the lower right corner of the window is the reference point. When dragging the window to a floating position, it is important that the lower right corner stays in the same position independent of window state. If the window now is dragged and snapped to the upper left position, it is equally important to keep the upper left position of the window in the same position independent of state.
I decided to keep the size of the internal window constant, the view that is. I thought that would be helpful in case the view is based on a dialog. Well, this decision didn't make moving any easier, since the outer size of the window now is changing depending on the snapping context or if the window is floating. Hence the elaborate reference point vs. offset for managing the dragging.
The classes CSnapWindowInfo
, CSnapTrackInfo
are used to manage state while tracking the mouse move events while in window drag mode. The template class CSnapWindowMover
is used in the floating and snapping window implementations and contains the logic for moving the window and shifting its state depending on location.
The template class CSnapFloatingWindowImpl
is implementing all events and logic associated with the floating window and is derived from CSnapWindowMover
.
typedef CWinTraits<WS_POPUPWINDOW|WS_CLIPSIBLINGS|
WS_OVERLAPPED|WS_THICKFRAME|WS_DLGFRAME,
WS_EX_TOOLWINDOW|WS_EX_WINDOWEDGE> CSnapFloatWinTraits;
template<class T, class TBase=CWindow, class TWinTraits=CSnapFloatWinTraits>
class ATL_NO_VTABLE CSnapFloatingWindowImpl :
public CWindowImpl< T, TBase, TWinTraits >,
public CSnapWindowMover<T>
class CSnapFloatingWindow : public CSnapFloatingWindowImpl<CSnapFloatingWindow>
The template class CSnapAutoHideWindowImpl
is implementing all events and logic associated with the snapping window. This window uses a timer to track if the mouse is outside an extended boundary, if so, it will "auto-hide" to the minibar state. The timer is only created for an expanded window that is not pinned and will be destroyed when going back to minibar state. CSnapAutoHideWindowImpl
is derived from CSnapWindowMover
.
typedef CWinTraits<WS_CHILD|WS_CLIPSIBLINGS|WS_CLIPCHILDREN|WS_THICKFRAME,
WS_EX_WINDOWEDGE> CSnapAutoHideWinTraits;
template<class T, class TBase=CWindow, class TWinTraits=CSnapAutoHideWinTraits>
class ATL_NO_VTABLE CSnapAutoHideWindowImpl :
public CWindowImpl< T, TBase, TWinTraits >,
public CSnapWindowMover<T>
enum { IDT_AUTOHIDE = 1234, IDT_INTERVAL = 500 };
class CSnapAutoHideWindow : public CSnapAutoHideWindowImpl<CSnapAutoHideWindow>
The template class CSnappingWindowImpl
is implementing all events and logic associated with the snapping window manager. As you can see in the code snippet below, snapped and floating window implementations can be replaced with your own extensions.
template<class T,
class TSnappedWindow = CSnapAutoHideWindow,
class TFloatingWindow = CSnapFloatingWindow,
class TBase = CWindow,
class TWinTraits = CControlWinTraits<
class ATL_NO_VTABLE CSnappingWindowImpl :
public CWindowImpl<T, TBase, TWinTraits>
class CSnappingWindow : public CSnappingWindowImpl<CSnappingWindow<
The floating and snapping window classes are created when a view is added to CSnappingWindow
instance using the AddSnappableWindow
member function.
SNAPCONTEXT* AddSnappableWindow(HWND hWndView)
{
ATLASSERT( ::IsWindow(hWndView) );
if (!::IsWindow(hWndView))
return NULL;
SNAPCONTEXT* pCtx = new SNAPCONTEXT;
::ZeroMemory(pCtx, sizeof(SNAPCONTEXT));
pCtx->hWndView = hWndView;
pCtx->hWndRoot = m_hWnd;
pCtx->dwFlags = snapHidden;
TSnappedWindow* wndSnapped = new TSnappedWindow(pCtx);
ATLASSERT(wndSnapped);
wndSnapped->Create(m_hWnd, rcDefault, NULL);
ATLASSERT(::IsWindow(wndSnapped->m_hWnd));
pCtx->hWndSnapped = *wndSnapped;
TFloatingWindow* wndFloating = new TFloatingWindow(pCtx);
ATLASSERT(wndFloating);
TCHAR szCaption[128];
::GetWindowText(hWndView, szCaption, sizeof(szCaption)/sizeof(TCHAR));
wndFloating->Create(m_hWnd, rcDefault, szCaption);
ATLASSERT(::IsWindow(wndFloating->m_hWnd));
pCtx->hWndFloated = *wndFloating;
m_snappableWindows.Add(pCtx);
return pCtx;
}
The default layout is calculated from the client window rectangle and taking scrollbars into consideration. See the image at the top of the article.
void QueryRect(RECT& rect)
{
T* pT = static_cast<T*>(this);
HWND hWndClient = pT->GetClient();
::GetWindowRect(hWndClient ,&rect);
LONG style = ::GetWindowLong(hWndClient,GWL_STYLE);
if (style & WS_VSCROLL)
{
rect.right -= ::GetSystemMetrics(SM_CXVSCROLL);
}
if (style & WS_HSCROLL)
{
rect.bottom -= ::GetSystemMetrics(SM_CYHSCROLL);
}
LONG styleEx = ::GetWindowLong(hWndClient,GWL_EXSTYLE);
if (styleEx & WS_EX_CLIENTEDGE)
::InflateRect(&rect, -2, -2);
}
To do
In no particular order:
- Persistence class to help store and retrieve positions between sessions.
- Synchronize active state between tool windows and frame window. This is built into MFC but not in WTL.
- Probably as part of item above, hide floating tool windows when parent frame is no longer active. It gets crowdie on screen when using floating tool windows and several instances of the application running.
Running the WTL application wizard to generate a SDI application with an Edit view created the demo application. Following that, I added the wtlsnappable header file and a view window to be snapped. CSnapView
, which is presented below, is just a dummy view window with a green background to show the features of the snapping framework. Notice that no extra code is needed here for the snapping framework.
class CSnapView : public CWindowImpl<CSnapView>
{
public: DECLARE_WND_CLASS(NULL)
BOOLPreTranslateMessage(MSG* pMsg)
{
pMsg;
return FALSE;
}
BEGIN_MSG_MAP(CSnapView)
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd)
END_MSG_MAP()
LRESULT OnEraseBkgnd(UINT, WPARAM wParam, LPARAM, BOOL&)
{
HDC dc = (HDC)wParam;
HBRUSH hBrush = ::CreateSolidBrush(RGB(0,128,0));
RECT rc;
GetClientRect(&rc);
::FillRect(dc,&rc, hBrush);
::DeleteObject(hBrush);
return 1;
}
};
CSnappingWindow
was added as a member to the CMainFrame
along with a few CSnapView
members. The views are added to the snapping window the in the WM_CREATE
message handler, see the code snippet below:
#include "wtlsnappable.h"
class CMainFrame : public CFrameWindowImpl<CMainFrame>,
public CUpdateUI<CMainFrame>,
public CMessageFilter, public CIdleHandler
{
public:
CSnappingWindow m_snapWindow;
CSnapView m_view1,m_view2,m_view3,m_view4,m_view5;
LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&)
{
m_hWndClient = m_snapWindow.Create(m_hWnd, rcDefault, NULL,
WS_CHILD|WS_CLIPSIBLINGS|WS_CLIPCHILDREN|WS_VISIBLE);
HWND hWndView = m_view.Create(m_hWndClient , rcDefault, NULL,
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS |
WS_CLIPCHILDREN | WS_HSCROLL | WS_VSCROLL |
ES_AUTOHSCROLL | ES_AUTOVSCROLL |
ES_MULTILINE | ES_NOHIDESEL, WS_EX_CLIENTEDGE);
m_snapWindow.SetClient(hWndView);
RECT rcView1 = {0,0,200,300};
RECT rcView2 = {0,0,200,200};
POINT ptFloat = {100,100};
m_view1.Create(m_hWnd, rcView1, _T("View 1"),
SNAP_DEFAULT_VIEW_STYLE, WS_EX_CLIENTEDGE);
m_snapWindow.AddSnappableWindow(m_view1);
m_snapWindow.FloatWindow(m_view1,ptFloat);
m_view2.Create(m_hWnd, rcView2, _T("View 2"),
SNAP_DEFAULT_VIEW_STYLE, WS_EX_CLIENTEDGE);
m_snapWindow.AddSnappableWindow(m_view2);
m_snapWindow.SnapWindow(m_view2, snapTopLeft);
m_view3.Create(m_hWnd, rcView1, _T("View 3"),
SNAP_DEFAULT_VIEW_STYLE, WS_EX_CLIENTEDGE);
m_snapWindow.AddSnappableWindow(m_view3);
m_snapWindow.SnapWindow(m_view3, snapTop, 100);
m_view4.Create(m_hWnd, rcView2, _T("View 4"),
SNAP_DEFAULT_VIEW_STYLE, WS_EX_CLIENTEDGE);
m_snapWindow.AddSnappableWindow(m_view4);
m_snapWindow.SnapWindow(m_view4,snapBottomRight,0,snapPinned);
m_view5.Create(m_hWnd, rcView2, _T("View 5"),
SNAP_DEFAULT_VIEW_STYLE, WS_EX_CLIENTEDGE);
m_snapWindow.AddSnappableWindow(m_view5);
m_snapWindow.SnapWindow(m_view5, snapTop, 350);
return 0;
}
};
That's all folks!